﻿using System;
using System.Collections.Generic;
using System.Linq;
using MongoDB.Bson;

namespace CQRS.Events.DomainClass
{
    public class AggregateRoot<TEvent> : IEventProvider<TEvent> where TEvent : Event
    {
        private readonly List<TEvent> _appliedEvents;
        private readonly Dictionary<Type, Action<TEvent>> _registeredEvents;

        public AggregateRoot()
        {
            _registeredEvents = new Dictionary<Type, Action<TEvent>>();
            _appliedEvents = new List<TEvent>();
            EventProviderId = Guid.NewGuid();
            RegisterEvents();
        }

        protected virtual void RegisterEvents()
        {
        }

        public int EventVersion { get; protected set; }

        #region IEventProvider<TEvent> Members

        public ObjectId Id { get; protected set; }

        public Guid EventProviderId { get; protected set; }

        public int Version { get; protected set; }

        void IEventProvider<TEvent>.LoadFromHistory(IList<TEvent> domainEvents)
        {
            if (domainEvents.Count == 0)
                return;

            foreach (TEvent domainEvent in domainEvents)
            {
                ApplyEvent(domainEvent.GetType(), domainEvent);
            }

            Version = domainEvents.Last().Version;
            EventVersion = Version;
        }

        IEnumerable<TEvent> IEventProvider<TEvent>.GetChanges()
        {
            return _appliedEvents.OrderBy(x => x.Version).ToList();
        }

        void IEventProvider<TEvent>.Clear()
        {
            _appliedEvents.Clear();
        }

        void IEventProvider<TEvent>.UpdateVersion(int version)
        {
            Version = version;
        }

        #endregion

        protected void RegisterEvent<TEvent>(Action<TEvent> eventHandler) where TEvent : class
        {
            _registeredEvents.Add(typeof(TEvent), @event => eventHandler(@event as TEvent));
        }

        protected void Apply(TEvent domainEvent)
        {
            Id = domainEvent.AggregateId;
            domainEvent.Version = ++EventVersion; 
            ApplyEvent(domainEvent.GetType(), domainEvent);
            _appliedEvents.Add(domainEvent);
        }

        private void ApplyEvent(Type eventType, TEvent domainEvent)
        { 
            Action<TEvent> handler;

            if (!_registeredEvents.TryGetValue(eventType, out handler))
                throw new UnknownEventException(
                    string.Format("The requested domain event '{0}' is not registered in '{1}'",
                                  eventType.FullName, GetType().FullName));

            handler(domainEvent);
        }
    }
}